/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
 *
 */

import { HVIGOR_ENABLE_MEMORY_CACHE, HVIGOR_MEMORY_THRESHOLD } from '../../../../common/const/const.js';
import { HvigorConfigLoader } from '../../../../common/util/hvigor-config-loader.js';

export class CacheStore {
  private caches: Map<string, any> = new Map<string, any>();
  private lruCache: LRUCache;
  private readonly name: string;
  private readonly pinned: boolean;
  private hvigorMemoryThreshold = HvigorConfigLoader.getInstance().getPropertiesConfigValue<number>(HVIGOR_MEMORY_THRESHOLD);
  private hvigorEnableMemoryCache = HvigorConfigLoader.getInstance().getPropertiesConfigValue<boolean>(HVIGOR_ENABLE_MEMORY_CACHE);

  constructor(name: string, lruCache: LRUCache, pinned = false) {
    this.name = name;
    this.lruCache = lruCache;
    this.pinned = pinned;
  }

  getCache(key: string): any {
    this.lruCache.access(this.completeKey(key));
    return this.caches.get(key);
  }

  get(key: string): any {
    return this.getCache(key);
  }

  set(key: string, value: any) {
    return this.setCache(key, value);
  }

  setCache(key: string, value: any, pinned = false): CacheStore {
    if ((this.hvigorMemoryThreshold && this.hvigorMemoryThreshold > 0) || this.hvigorEnableMemoryCache === false) {
      return this;
    }
    const isNewCache = !this.caches.has(key);
    this.caches.set(key, value);
    if (pinned || this.pinned) {
      return this;
    }
    const deletedKey = this.lruCache.set(this.completeKey(key));
    deletedKey && this.caches.delete(this.parseCompleteKey(deletedKey));
    if (this.lruCache.getTTL() && isNewCache) {
      setTimeout(() => this.delete(key), this.lruCache.getTTL());
    }
    return this;
  }

  hasCache(key: string): boolean {
    return this.caches.has(key);
  }

  clear() {
    this.caches.forEach((_, key) => {
      this.lruCache.delete(this.completeKey(key));
    });
    this.caches.clear();
  }

  delete(key: string): boolean {
    this.lruCache.delete(this.completeKey(key));
    return this.caches.delete(key);
  }

  size(): number {
    return this.caches.size;
  }

  keys(): IterableIterator<string> {
    return this.caches.keys();
  }

  private completeKey(key: string) {
    return `${this.name}@${key}`;
  }

  private parseCompleteKey(key: string) {
    return key.substring(this.name.length + 1);
  }
}

export interface CacheStoreManagerOption {
  capacity?: number;
  ttl?: number;
}

export class CacheStoreManager {
  private cacheStores: Map<string, CacheStore> = new Map<string, CacheStore>();
  private readonly lruCache: LRUCache;

  constructor(cacheStoreManagerOption?: CacheStoreManagerOption) {
    this.lruCache = new LRUCache(cacheStoreManagerOption);
  }
  mount(key: string, pinned = false): CacheStore {
    if (!this.cacheStores.has(key)) {
      this.cacheStores.set(key, new CacheStore(key, this.lruCache, pinned));
    }

    // Safety: 在上面已经判断key是否存在
    return this.cacheStores.get(key)!;
  }

  unmount(key: string): boolean {
    this.cacheStores.get(key)?.clear();
    return this.cacheStores.delete(key);
  }

  clear() {
    this.lruCache.clear();
    this.cacheStores.clear();
  }

  size() {
    return this.cacheStores.size;
  }

  keys(): IterableIterator<string> {
    return this.cacheStores.keys();
  }

  cacheItemSize() {
    return Array.from(this.cacheStores.values()).reduce((cnt, cacheStore) => cnt + cacheStore.size(), 0);
  }
}

class LinkedNode {
  key: string;
  pre: LinkedNode | undefined;
  next: LinkedNode | undefined;
  birth: number;

  constructor(k: string) {
    this.key = k;
    this.birth = Date.now();
  }
}

class LRUCache {
  private cnt: number;
  private readonly capacity: number | undefined;
  private key2node: Map<string, LinkedNode>;
  private readonly head: LinkedNode;
  private readonly tail: LinkedNode;
  private readonly ttl: number | undefined;

  constructor(cacheStoreManagerOption?: CacheStoreManagerOption) {
    this.cnt = 0;
    this.capacity = cacheStoreManagerOption?.capacity ?? 4;
    this.ttl = cacheStoreManagerOption?.ttl ?? 60 * 60 * 1000;
    this.key2node = new Map<string, LinkedNode>();
    this.head = new LinkedNode('');
    this.tail = new LinkedNode('');
    this.head.next = this.tail;
    if (this.ttl && this.ttl < 0) {
      this.ttl = undefined;
    }
    if (this.capacity && this.capacity < 0) {
      this.capacity = undefined;
    }
  }

  access(key: string) {
    const linkedNode = this.key2node.get(key);
    if (linkedNode) {
      this.moveToHead(linkedNode);
    }
  }

  set(key: string) {
    const node = this.key2node.get(key);
    if (node) {
      this.moveToHead(node);
      return;
    }
    const newNode = new LinkedNode(key);

    // Safety: head.next必然非undefined
    const temp: LinkedNode = this.head.next!;
    this.head.next = newNode;
    newNode.next = temp;
    newNode.pre = this.head;
    temp.pre = newNode;
    this.key2node.set(key, newNode);
    this.cnt++;
    return this.eliminate();
  }

  delete(key: string) {
    const deletedNode = this.key2node.get(key);
    if (!deletedNode) {
      return;
    }

    // Safety: deletedNode是中间节点，必然有pre和next
    deletedNode.pre!.next = deletedNode.next;
    deletedNode.next!.pre = deletedNode.pre;
    this.key2node.delete(key);
    this.cnt--;
  }

  private moveToHead(node: LinkedNode) {
    // Safety: node是中间节点，必然有pre和next
    node.pre!.next = node.next;
    node.next!.pre = node.pre;
    const temp = this.head.next;
    this.head.next = node;
    node.next = temp;
    node.pre = this.head;
    temp!.pre = node;
  }

  private eliminate() {
    if (this.capacity === undefined || this.cnt <= this.capacity) {
      return;
    }

    // Safety: tail.pre必然非undefined
    const last: LinkedNode = this.tail.pre!;
    this.key2node.delete(last.key);

    // Safety: cnt > 0，所以tail.pre.pre必然非undefined
    last.pre!.next = this.tail;
    this.tail.pre = last.pre;
    this.cnt--;
    return last.key;
  }

  clear() {
    this.key2node.clear();
    this.cnt = 0;
    this.head.next = this.tail;
    this.tail.pre = this.tail.next = this.head.pre = undefined;
  }

  getTTL() {
    return this.ttl;
  }
}
